<?php

namespace Xrfg\Api\Trading\Spot\Controllers;

use App\Http\Controllers\Controller;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Http\Response;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\Redis;
use PDOException;
use Xrfg\Core\Trading\Spot\Models\Kline;
use Xrfg\Core\Trading\Spot\Models\Trade;
use Xrfg\Api\Trading\Spot\Repositories\OrderBook;
use Xrfg\Api\Trading\Spot\Requests\DepthRequest;
use Xrfg\Api\Trading\Spot\Requests\KlineRequest;
use Xrfg\Api\Trading\Spot\Requests\LastTradeRequest;
use Xrfg\Api\Trading\Spot\Resources\KlineResource;
use Xrfg\Api\Trading\Spot\Resources\LastTradeResource;

class MarketController extends Controller
{
    /**
     * K线
     *
     * @param KlineRequest $request
     *
     * @return \Illuminate\Http\Resources\Json\AnonymousResourceCollection|Response
     */
    public function kline(KlineRequest $request)
    {
        $symbol = $request->query('symbol');
        $limit = $request->query('limit');
        $from = $request->query('from');
        $to = $request->query('to');
        $range = $request->query('range');

        if (in_array($symbol, array_keys(config('huobi')))) {
            $type = $range / 60000;
            switch ($type) {
                case 1:
                    $time = '1min';
                    break;
                case 5:
                    $time = '5min';
                    break;
                case 15:
                    $time = '15min';
                    break;
                case 30:
                    $time = '30min';
                    break;
                case 60:
                    $time = '60min';
                    break;
                case 240:
                    $time = '4hour';
                    break;
                case 1440:
                    $time = '1day';
                    break;
                case 10080:
                    $time = '1week';
                    break;
            }
            $kline = self::huobiKline($symbol, $time, $limit, $from, $to);
            if (!$kline) {
                $kline = [];
            }

            return $this->array($kline);
        } else {
            $kline = Cache::remember(
                sprintf('res:kline:%s:%s:%s:%s:%s', $symbol, $range, $limit, $from, $to),
                now()->addSecond(),
                function () use ($symbol, $range, $limit, $from, $to) {
                    $type = $range / 60000;
                    $limit = (empty($limit) or is_null($limit)) ? 500 : ($limit > 500 ? 500 : (int) $limit);
                    $from = (empty($from) or is_null($from)) ? false : (int) $from;
                    $to = (empty($to) or is_null($to)) ? false : (int) $to;
                    try {
                        $kline = (new Kline($symbol, $type))
                            ->when($from, function (Builder $query) use ($from) {
                                return $query->where('time_open', '>=', $from);
                            })
                            ->when($to, function (Builder $query) use ($to) {
                                return $query->where('time_open', '<=', $to);
                            })
                            ->orderBy('time_open', 'desc')
                            ->limit($limit)
                            ->get();

                        return $kline;
                    } catch (PDOException $exception) {
                        return collect();
                    }
                }
            );

            return KlineResource::collection($kline);
        }
    }

    /**
     * 最后一根K线
     *
     * @param KlineRequest $request
     *
     * @return \Illuminate\Http\Resources\Json\AnonymousResourceCollection|Response
     */
    public function lastKline(KlineRequest $request)
    {
        $symbol = $request->query('symbol');
        $limit = $request->query('limit');
        $from = $request->query('from');
        $to = $request->query('to');
        $range = $request->query('range');

        if (in_array($symbol, array_keys(config('huobi')))) {
            $type = $range / 60000;
            switch ($type) {
                case 1:
                    $time = '1min';
                    break;
                case 5:
                    $time = '5min';
                    break;
                case 15:
                    $time = '15min';
                    break;
                case 30:
                    $time = '30min';
                    break;
                case 60:
                    $time = '60min';
                    break;
                case 240:
                    $time = '4hour';
                    break;
                case 1440:
                    $time = '1day';
                    break;
                case 10080:
                    $time = '1week';
                    break;
            }
            $kline = self::huobiKline($symbol, $time, 1, $from, $to);
            if (!$kline) {
                $kline = [];
            }

            return $this->array($kline);
        } else {
            $kline = Cache::remember(
                sprintf('res:kline:%s:%s:%s:%s:%s', $symbol, $range, $limit, $from, $to),
                now()->addSecond(),
                function () use ($symbol, $range, $limit, $from, $to) {
                    $type = $range / 60000;
                    $limit = (empty($limit) or is_null($limit)) ? 500 : ($limit > 500 ? 500 : (int) $limit);
                    $from = (empty($from) or is_null($from)) ? false : (int) $from;
                    $to = (empty($to) or is_null($to)) ? false : (int) $to;
                    try {
                        $kline = (new Kline($symbol, $type))
                            ->when($from, function (Builder $query) use ($from) {
                                return $query->where('time_open', '>=', $from);
                            })
                            ->when($to, function (Builder $query) use ($to) {
                                return $query->where('time_open', '<=', $to);
                            })
                            ->orderBy('time_open', 'desc')
                            ->limit(1)
                            ->get();

                        return $kline;
                    } catch (PDOException $exception) {
                        return collect();
                    }
                }
            );

            return KlineResource::collection($kline);
        }
    }

    private function huobiKline($symbol, $time, $limit, $from, $to)
    {
        $symbol = strtolower(str_replace('/', '', $symbol));
        $redis = Redis::connection();
        $result = $redis->zrange("huobi:kline:{$symbol}:{$time}", -300, -1);

        return collect($result)->map(function ($item) {
            $item = json_decode($item);

            return [
                'time' => $item[0],
                'open' => (float) $item[1],
                'high' => (float) $item[2],
                'low' => (float) $item[3],
                'close' => (float) $item[4],
                'volume' => (float) $item[5],
                'trades_total' => (float) $item[6],
                'trades_count' => $item[7],
            ];
        })->where('time', '>=', $from)->where('time', '<=', $to ?: PHP_INT_MAX)->sortByDesc('time')->take($limit)->values()->all();
    }

    /**
     * 深度.
     *
     * @param OrderBook    $orderBook
     * @param DepthRequest $request
     *
     * @return Response
     */
    public function depth(OrderBook $orderBook, DepthRequest $request)
    {
        $symbol = $request->query('symbol');
        $size = $request->query('size') ?? 20;
        if (in_array($symbol, array_keys(config('huobi')))) {
//            return $this->array(self::huobiDepth($symbol, $size));
            $huobiDepth = self::huobiDepth($symbol, $size);
            $etfDepth = self::etfDepth($symbol, $size);
            // 2.本地已成交的订单，从火币深度数据删除
            $key = "etf:trade:{$symbol}";
            $redis = Redis::connection();
            $trade = $redis->lindex($key, 0);
            // 1.本地没成交的订单，合并到火币深度数据
            $buys = $etfDepth['bids'];
            $sells = $etfDepth['asks'];

            if ($sells) { // 合并卖单
                $asks = $huobiDepth['asks'];
                $arr = []; // 卖出价格关联数组
                foreach ($asks as $ask) {
                    $arr[$ask[0]] = $ask[1];
                }
                // 本地深度插入关联数组
                foreach ($sells as $sell) {
                    if (in_array($sell[0], array_keys($arr))) { // 有相同价格
                        $arr[$sell[0]] = bcadd($arr[$sell[0]], $sell[1]);
                    } else { // 没有相同价格
                        $arr[$sell[0]] = $sell[1];
                    }
                }
                // 删除已成交记录
                if ($trade) {
                    $trade1 = json_decode($trade, true);
                    if ($trade1['side'] == 'SELL') {
                        if (now()->timestamp - $trade1['time'] <= 10) {
                            foreach ($arr as $k => $v) {
                                if ($k < $trade1['price']) {
                                    unset($arr[$k]);
                                }
                                if ($k == $trade1['price']) {
                                    $arr[$k] = bcsub($v, $trade1['number']);
                                    if ($arr[$k] <= 0) {
                                        unset($arr[$k]);
                                    }
                                }
                            }
                        }
                    }
                }
                ksort($arr);
                $asks = collect($arr)
                    ->slice(0, $size)
                    ->transform(function ($number, $price) {
                        return [
                            number_format($price, 8, '.', ''),
                            number_format($number, 8, '.', ''),
                        ];
                    })
                    ->values()
                    ->toArray();
                $huobiDepth['asks'] = $asks;
            }

            if ($buys) { // 合并买单
                $bids = $huobiDepth['bids'];
                $arr = []; // 买入价格关联数组
                foreach ($bids as $bid) {
                    $arr[$bid[0]] = $bid[1];
                }
                // 本地深度插入关联数组
                foreach ($buys as $buy) {
                    if (in_array($buy[0], array_keys($arr))) { // 有相同价格
                        $arr[$buy[0]] = bcadd($arr[$buy[0]], $buy[1]);
                    } else { // 没有相同价格
                        $arr[$buy[0]] = $buy[1];
                    }
                }
                // 删除已成交记录
                if ($trade) {
                    $trade2 = json_decode($trade, true);
                    if ($trade2['side'] == 'BUY') {
                        if (now()->timestamp - $trade2['time'] <= 10) {
                            foreach ($arr as $k => $v) {
                                if ($k > $trade2['price']) {
                                    unset($arr[$k]);
                                }
                                if ($k == $trade2['price']) {
                                    $arr[$k] = bcsub($v, $trade2['number']);
                                    if ($arr[$k] <= 0) {
                                        unset($arr[$k]);
                                    }
                                }
                            }
                        }
                    }
                }
                krsort($arr);
                $bids = collect($arr)
                    ->slice(0, $size)
                    ->transform(function ($number, $price) {
                        return [
                            number_format($price, 8, '.', ''),
                            number_format($number, 8, '.', ''),
                        ];
                    })
                    ->values()
                    ->toArray();
                $huobiDepth['bids'] = $bids;
            }

            // 加入最新价一起返回
            $redis = Redis::connection();
            $symbol = strtolower(str_replace('/', '', $symbol));
            $ticker = $redis->get('huobi:ticker:'.$symbol);
            return $this->array(array_merge($huobiDepth, ['now' => $ticker ? json_decode($ticker, true)['close'] : '0.00000000']));
        }

        // 加入最新价一起返回
        $redis = Redis::connection();
        $ticker = $redis->get('etf:ticker:'.$symbol);
        return $this->array(array_merge(self::etfDepth($symbol, $size), ['now' => $ticker ? json_decode($ticker, true)['close'] : '0.00000000']));
    }

    private function huobiDepth($symbol, $size)
    {
        $symbol = strtolower(str_replace('/', '', $symbol));
        $redis = Redis::connection();

        $result = $redis->zrange('huobi:depth:'.$symbol.':bids', 0, -1, ['WITHSCORES' => true]);
        if ($result) {
            krsort($result);
            $bids = collect($result)
                ->slice(0, $size)
                ->transform(function ($number, $price) {
                    return [
                        number_format($price, 8, '.', ''),
                        number_format($number, 8, '.', ''),
                    ];
                })
                ->values()
                ->toArray();
        } else {
            $bids = [];
        }

        $result = $redis->zrange('huobi:depth:'.$symbol.':asks', 0, -1, ['WITHSCORES' => true]);
        if ($result) {
            ksort($result);
            $asks = collect($result)
                ->slice(0, $size)
                ->transform(function ($number, $price) {
                    return [
                        number_format($price, 8, '.', ''),
                        number_format($number, 8, '.', ''),
                    ];
                })
                ->values()
                ->toArray();
        } else {
            $asks = [];
        }

        return compact('asks', 'bids');
    }

    private function etfDepth($symbol, $size)
    {
        $depth = [];
        $data = (new OrderBook)->depth($symbol);
        $buys = array_flip($data['buys']);
        $sells = array_flip($data['sells']);
        krsort($buys);
        ksort($sells);
        $buys = array_slice($buys, 0, $size, true);
        $sells = array_slice($sells, 0, $size, true);

        if (!$buys) {
            $depth['bids'] = []; // 买入
        } else {
            foreach ($buys as $price => $amounts) {
                $num = collect(json_decode($amounts))->reduce(function ($num, $item) {
                    return $num + $item[1];
                }, 0);
                $depth['bids'][] = [
                    number_format($price, 8, '.', ''),
                    number_format($num, 8, '.', ''),
                ]; // 买入
            }
        }

        if (!$sells) {
            $depth['asks'] = []; // 卖出
        } else {
            foreach ($sells as $price => $amounts) {
                $num = collect(json_decode($amounts))->reduce(function ($num, $item) {
                    return $num + $item[1];
                }, 0);
                $depth['asks'][] = [
                    number_format($price, 8, '.', ''),
                    number_format($num, 8, '.', ''),
                ]; // 卖出
            }
        }

        return $depth;
    }

    /**
     * 最近成交.
     *
     * @param LastTradeRequest $request
     *
     * @return \Illuminate\Http\Resources\Json\AnonymousResourceCollection|Response
     */
    public function trades(LastTradeRequest $request)
    {
        $symbol = $request->input('symbol');
        if (in_array($symbol, array_keys(config('huobi')))) {
            return $this->array(self::huobiTrade($symbol));
        }
        $limit = (int) $request->query('limit', 100);

        $trades = Trade::select(['id', 'side', 'price', 'number', 'created_at'])
            ->where('symbol', $symbol)
            ->orderBy('id', 'desc')
            ->limit($limit)
            ->get();

        return LastTradeResource::collection($trades);
    }

    private function huobiTrade($symbol)
    {
        $symbol = strtolower(str_replace('/', '', $symbol));
        $redis = Redis::connection();
        $trades = $redis->lrange('huobi:trade:'.$symbol, 0, -1);
        if (!$trades) {
            return [];
        } else {
            return collect($trades)->map(function ($trade) {
                return json_decode($trade);
            })->toArray();
        }
    }

}
